/*
 * Zmanim Java API
 * Copyright (C) 2004-2007 Eliyahu Hershfeld
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
 */
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;
using KosherJava.Zmanim;
using PublicDomain;
using System.Linq;

namespace KosherJava.Zmanim.Util
{

    /**
     * A class used to format non {@link java.util.Date} times generated by the
     * Zmanim package. For example the
     * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour()} returns
     * the length of the hour in milliseconds. This class can format this time.
     * 
     * @author &copy; Eliyahu Hershfeld 2004 - 2007
     * @version 0.9.0
     */
    public class ZmanimFormatter
    {
        private bool prependZeroHours;

        private bool useSeconds;

        private bool useMillis;

        bool useDecimal;
        private string dateTimeFormater;

        // private DecimalFormat decimalNF;

        /**
         * Format using hours, minutes, seconds and milliseconds using the xsd:time
         * format. This format will return 00.00.00.0 when formatting 0.
         */
        public const int SEXAGESIMAL_XSD_FORMAT = 0;

        private int timeFormat = SEXAGESIMAL_XSD_FORMAT;

        /**
         * Format using standard decimal format with 5 positions after the decimal.
         */
        public const int DECIMAL_FORMAT = 1;

        /** Format using hours and minutes. */
        public const int SEXAGESIMAL_FORMAT = 2;

        /** Format using hours, minutes and seconds. */
        public const int SEXAGESIMAL_SECONDS_FORMAT = 3;

        /** Format using hours, minutes, seconds and milliseconds. */
        public const int SEXAGESIMAL_MILLIS_FORMAT = 4;

        /** constant for milliseconds in a minute (60,000) */
        const long MINUTE_MILLIS = 60 * 1000;

        /** constant for milliseconds in an hour (3,600,000) */
        const long HOUR_MILLIS = MINUTE_MILLIS * 60;

        /**
         * Format using the XSD Duration format. This is in the format of
         * PT1H6M7.869S (P for period (duration), T for time, H, M and S indicate
         * hours, minutes and seconds.
         */
        public static readonly int XSD_DURATION_FORMAT = 5;

        public ZmanimFormatter() :
            this(0, "h:mm:ss")
        {
        }

        /**
         * ZmanimFormatter constructor using a formatter
         * 
         * @param format
         *            int The formatting style to use. Using
         *            ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the
         *            time time of 90*60*1000 + 1 as 1:30:00
         */
        public ZmanimFormatter(int format, string dateTimeFormat)
        {
            setTimeFormat(format);
            setDateFormat(dateTimeFormat);
        }

        /**
         * Sets the format to use for formatting.
         * 
         * @param format
         *            int the format constant to use.
         */
        public void setTimeFormat(int format)
        {
            timeFormat = format;
            switch (format)
            {
                case SEXAGESIMAL_XSD_FORMAT:
                    setSettings(true, true, true);
                    break;
                case SEXAGESIMAL_FORMAT:
                    setSettings(false, false, false);
                    break;
                case SEXAGESIMAL_SECONDS_FORMAT:
                    setSettings(false, true, false);
                    break;
                case SEXAGESIMAL_MILLIS_FORMAT:
                    setSettings(false, true, true);
                    break;
                case DECIMAL_FORMAT:
                default:
                    useDecimal = true;
                    break;
            }
        }

        public void setDateFormat(string sdf)
        {
            dateTimeFormater = sdf;
        }

        public string getDateFormat()
        {
            return dateTimeFormater;
        }

        private void setSettings(bool prependZeroHours, bool useSeconds,
                bool useMillis)
        {
            this.prependZeroHours = prependZeroHours;
            this.useSeconds = useSeconds;
            this.useMillis = useMillis;
        }

        /**
         * A method that formats milliseconds into a time format.
         * 
         * @param milliseconds
         *            The time in milliseconds.
         * @return String The formatted <code>String</code>
         */
        public String format(double milliseconds)
        {
            return format((int)milliseconds);
        }

        /**
         * A method that formats milliseconds into a time format.
         * 
         * @param millis
         *            The time in milliseconds.
         * @return String The formatted <code>String</code>
         */
        public String format(int millis)
        {
            return format(new Time(millis));
        }

        /**
         * A method that formats {@link Time}objects.
         * 
         * @param time
         *            The time <code>Object</code> to be formatted.
         * @return String The formatted <code>String</code>
         */
        public String format(Time time)
        {
            if (timeFormat == XSD_DURATION_FORMAT)
            {
                return formatXSDDurationTime(time);
            }
            StringBuilder sb = new StringBuilder();
            sb.Append(time.getHours().ToString(prependZeroHours ? "00" : "0"));
            sb.Append(":");
            sb.Append(time.getMinutes().ToString("00"));
            if (useSeconds)
            {
                sb.Append(":");
                sb.Append(time.getSeconds().ToString("00"));
            }
            if (useMillis)
            {
                sb.Append(".");
                sb.Append(time.getMilliseconds().ToString("000"));
            }
            return sb.ToString();
        }

        /**
         * Formats a date using this classe's {@link #getDateFormat() date format}.
         * 
         * @param dateTime
         *            the date to format
         * @param calendar
         *            the {@link java.util.Calendar Calendar} used to help format
         *            based on the Calendar's DST and other settings.
         * @return the formatted String
         */
        public String formatDateTime(TzDateTime dateTime, Calendar calendar)
        {
            if (dateTimeFormater == "yyyy-MM-dd'T'HH:mm:ss")
            {
                return getXSDateTime(dateTime, calendar);
            }
            else
            {
                return dateTime.ToString(dateTimeFormater);
            }
        }

        /**
         * The date:date-time function returns the current date and time as a
         * date/time string. The date/time string that's returned must be a string
         * in the format defined as the lexical representation of xs:dateTime in <a
         * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a>
         * of <a href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
         * Datatypes]</a>. The date/time format is basically CCYY-MM-DDThh:mm:ss,
         * although implementers should consult <a
         * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
         * Datatypes]</a> and <a href="http://www.iso.ch/markete/8601.pdf">[ISO
         * 8601]</a> for details. The date/time string format must include a time
         * zone, either a Z to indicate Coordinated Universal Time or a + or -
         * followed by the difference between the difference from UTC represented as
         * hh:mm.
         */
        public String getXSDateTime(TzDateTime dateTime, Calendar cal)
        {
            String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";

            StringBuilder buff = new StringBuilder(dateTime.ToString(xsdDateTimeFormat));
            // Must also include offset from UTF.
            // Get the offset (in milliseconds).
            int offset = cal.get(Zmanim.ZONE_OFFSET)
                    + cal.get(Zmanim.DST_OFFSET);
            // If there is no offset, we have "Coordinated
            // Universal Time."
            if (offset == 0)
                buff.Append("Z");
            else
            {
                // Convert milliseconds to hours and minutes
                int hrs = offset / (60 * 60 * 1000);
                // In a few cases, the time zone may be +/-hh:30.
                int min = offset % (60 * 60 * 1000);
                char posneg = hrs < 0 ? '-' : '+';
                buff.Append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
            }
            return buff.ToString();
        }

        /**
         * Represent the hours and minutes with two-digit strings.
         * 
         * @param digits hours or minutes.
         * @return two-digit String representation of hrs or minutes.
         */
        private static String formatDigits(int digits)
        {
            String dd = Math.Abs(digits).ToString();
            return dd.Length == 1 ? '0' + dd : dd;
        }

        /**
         * This returns the xml representation of an xsd:duration object.
         * 
         * @param millis the duration in milliseconds
         * @return the xsd:duration formatted String
         */
        public String formatXSDDurationTime(long millis)
        {
            return formatXSDDurationTime(new Time(millis));
        }

        /**
         * This returns the xml representation of an xsd:duration object.
         * 
         * @param time the duration as a Time object 
         * @return the xsd:duration formatted String
         */
        public String formatXSDDurationTime(Time time)
        {
            StringBuilder duration = new StringBuilder();

            duration.Append("P");

            if (time.getHours() != 0 || time.getMinutes() != 0
                    || time.getSeconds() != 0 || time.getMilliseconds() != 0)
            {
                duration.Append("T");

                if (time.getHours() != 0)
                    duration.Append(time.getHours() + "H");

                if (time.getMinutes() != 0)
                    duration.Append(time.getMinutes() + "M");

                if (time.getSeconds() != 0 || time.getMilliseconds() != 0)
                {
                    duration.Append(time.getSeconds() + "."
                            + milliNF.format(time.getMilliseconds()));
                    duration.Append("S");
                }
                if (duration.Length == 1) // zero seconds
                    duration.Append("T0S");
                if (time.isNegative())
                    duration.Insert(0, "-");
            }
            return duration.ToString();
        }

        /**
         * A method that returns an XML formatted <code>String</code> representing
         * the serialized <code>Object</code>. The format used is:
         * 
         * <pre>
         *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
         *     ...
         *   &lt;/AstronomicalTimes&gt;
         * </pre>
         * 
         * Note that the output uses the <a
         * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a>
         * format for times such as sunrise, and <a
         * href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a>
         * format for times that are a duration such as the length of a
         * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
         * returned by the {@link #toString() toString} }.
         * 
         * @return The XML formatted <code>String</code>. The format will be:
         * 
         * <pre>
         *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
         *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
         *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
         *     ...
         *  &lt;/AstronomicalTimes&gt;
         * </pre>
         * 
         */
        public static String toXML(AstronomicalCalendar ac)
        {
            ZmanimFormatter formatter = new ZmanimFormatter(XSD_DURATION_FORMAT, "yyyy-MM-dd'T'HH:mm:ss");
            string df = "yyyy-MM-dd";
            String output = "<";
            if (ac.GetType().Name.EndsWith("AstronomicalCalendar"))
            {
                output += "AstronomicalTimes";
            }
            else if (ac.GetType().Name.EndsWith("ZmanimCalendar"))
            {
                output += "Zmanim";
            }
            output += " date=\"" + ac.getCalendar().getTime().ToString(df) + "\"";
            output += " type=\"" + ac.GetType().Name + "\"";
            output += " algorithm=\""
                    + ac.getAstronomicalCalculator().getCalculatorName() + "\"";
            output += " location=\"" + ac.getGeoLocation().getLocationName() + "\"";
            output += " latitude=\"" + ac.getGeoLocation().getLatitude() + "\"";
            output += " longitude=\"" + ac.getGeoLocation().getLongitude() + "\"";
            output += " elevation=\"" + ac.getGeoLocation().getElevation() + "\"";
            output += " timeZoneName=\""
                    + ac.getGeoLocation().getTimeZone().getDisplayName() + "\"";
            output += " timeZoneID=\"" + ac.getGeoLocation().getTimeZone().getID()
                    + "\"";
            output += " timeZoneOffset=\""
                    + (ac.getGeoLocation().getTimeZone().getOffset(
                            ac.getCalendar().getTimeInMillis()) / (HOUR_MILLIS))
                    + "\"";

            output += ">\n";

            IList<MethodInfo> theMethods = new List<MethodInfo>(ac.GetType().GetMethods());
            String tagName = "";
            Object value = null;
            IList<Zman> dateList = new List<Zman>();
            IList<Zman> durationList = new List<Zman>();
            IList<string> otherList = new List<string>();

            for (int i = 0; i < theMethods.Count; i++)
            {
                if (includeMethod(theMethods[i]))
                {
                    tagName = theMethods[i].Name.Substring(3);
                    //String returnType = theMethods[i].getReturnType().getName();
                    try
                    {
                        value = theMethods[i].Invoke(ac, null);
                        if (value == null)
                        {//FIXME: use reflection to determine what the return type is, not the value
                            otherList.Add("<" + tagName + ">N/A</" + tagName + ">");
                        }
                        else if (value.GetType() == typeof(TzDateTime))
                        {
                            dateList.Add(new Zman((TzDateTime)value, tagName));
                        }
                        else if (value.GetType() == typeof(long))
                        {// shaah zmanis
                            durationList.Add(new Zman((int)((long)value), tagName));
                        }
                        else
                        { // will probably never enter this block, but is
                            // present to be future proof
                            otherList.Add("<" + tagName + ">" + value + "</" + tagName + ">");
                        }
                    }
                    catch (Exception e)
                    {
                        //e.printStackTrace();
                        throw;
                    }
                }
            }

            foreach (var zman in dateList.OrderBy(x => x.getZman()))
            {
                output += "\t<" + zman.getZmanLabel();
                output += ">";
                output += formatter.formatDateTime(zman.getZman(), ac.getCalendar())
                        + "</" + zman.getZmanLabel() + ">\n";
            }

            foreach (var zman in dateList.OrderBy(x => x.getDuration()))
            {
                output += "\t<" + zman.getZmanLabel();
                output += ">";
                output += formatter.format((int)zman.getDuration()) + "</"
                        + zman.getZmanLabel() + ">\n";
            }

            for (int i = 0; i < otherList.Count; i++)
            {   // will probably never enter
                // this block
                output += "\t" + otherList[i] + "\n";
            }

            if (ac.GetType().Name.EndsWith("AstronomicalCalendar"))
            {
                output += "</AstronomicalTimes>";
            }
            else if (ac.GetType().Name.EndsWith("ZmanimCalendar"))
            {
                output += "</Zmanim>";
            }
            return output;
        }

        /**
         * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
         * 
         * @param method
         * @return
         */
        private static bool includeMethod(MethodInfo method)
        {
            IList<string> methodWhiteList = new List<string>();
            // methodWhiteList.add("getName");

            IList<string> methodBlackList = new List<string>();
            // methodBlackList.add("getGregorianChange");

            if (methodWhiteList.Contains(method.Name))
                return true;
            if (methodBlackList.Contains(method.Name))
                return false;

            if (method.GetParameters().Length > 0)
                return false; // Skip get methods with parameters since we do not
            // know what value to pass

            if (!method.Name.StartsWith("get"))
                return false;

            if (method.ReturnType.Name.EndsWith("Date")
                    || method.ReturnType.Name.EndsWith("long"))
            {
                return true;
            }
            return false;
        }
    }
}